﻿using System.Text.RegularExpressions;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace PDO.DataPersistence.Core
{
    public class PDOExpressionEvaluator
    {

        private const string ExceptionMsgHeader = "ExpressionEvaluator: ";

        #region Static Fields

        private static string[] _binaryOperators = { "*", "/", "+", "-", "&&", "||", "=", "==", "!=" };
        private static string[] _unaryOperators = { "!" };

        private static string[] _separators = { "*", "/", "+", "-", "&&", "||", "==", "!=", "=", "(", ")", "!", "[", "]" };

        private static Hashtable _precedence = new Hashtable();

        #endregion

        protected object _dataContext;
        private string _expression;

        #region Private Fields

        private string[] _infix;
        private string[] _postfix;
        private Stack<string> _stack = new Stack<string>();
        private int _postIndex = 0;
        private Hashtable _symbolTable; // possibly use for intermediate results in the future

        #endregion

        static PDOExpressionEvaluator()
        {
            // Add precedence of operators
            _precedence.Add("!", 0);
            _precedence.Add("*", 1);
            _precedence.Add("/", 2);
            _precedence.Add("+", 3);
            _precedence.Add("-", 4);
            _precedence.Add("!=", 5);
            _precedence.Add("=", 6);
            _precedence.Add("==", 7);
            _precedence.Add("&&", 8);
            _precedence.Add("||", 9);
            _precedence.Add("(", 11);

            //precedence.Add(")", 10); //???
        }

        public PDOExpressionEvaluator()
        {
        }


        private void Tokenize(string exp)
        {
            List<string> infix = new List<string>();

            char[] expA = exp.ToCharArray();
            char[] tokenA = new char[1000];
            int expIndex = 0;
            StringBuilder tsb = new StringBuilder();
            string token;
            bool inQuote = false;

            infix.Add("(");

            for (expIndex = 0; expIndex < exp.Length; expIndex++)
            {
                char e = expA[expIndex];

                if (e == '"')
                {
                    inQuote = !inQuote;
                }

                if (inQuote)
                {
                    tsb.Append(e);
                }
                else
                {

                    string sep = new string(e, 1);
                    bool isSep = false;
                    foreach (string seperator in _separators)
                    {
                        if (seperator.StartsWith(sep))
                        {
                            int expSepIndex = expIndex;
                            while (sep.Length < seperator.Length)
                            {
                                sep += expA[++expSepIndex];
                            }

                            if (seperator == sep)
                            {
                                token = tsb.ToString();
                                tsb.Length = 0;

                                infix.Add(token);
                                infix.Add(sep);

                                expIndex = expSepIndex;
                                isSep = true;

                                break;
                            }
                        }
                    }

                    if (!isSep)
                    {
                        tsb.Append(e);
                    }
                }
            }

            if (tsb.Length > 0)
            {
                infix.Add(tsb.ToString());
            }

            infix.Add(")");

            _infix = infix.ToArray();

            // Convert token array to postfix form tokens
            InfixToPostfix();
        }



        private void tok2(string exp)
        {
            List<string> infix = new List<string>();

            char[] expA = exp.ToCharArray();
            char[] tokenA = new char[1000];
            int expIndex = 0;
            StringBuilder tsb = new StringBuilder();
            string token;
            bool inQuote = false;

            infix.Add("(");

            for (expIndex = 0; expIndex < exp.Length; expIndex++)
            {
                char e = expA[expIndex];

                if (e == '"')
                {
                    inQuote = !inQuote;
                }

                if (inQuote)
                {
                    tsb.Append(e);
                }
                else
                {

                    string sep = new string(e, 1);
                    bool isSep = false;
                    foreach (string seperator in _separators)
                    {
                        if (seperator.StartsWith(sep))
                        {
                            int expSepIndex = expIndex;
                            while (sep.Length < seperator.Length)
                            {
                                sep += expA[++expSepIndex];
                            }

                            if (seperator == sep)
                            {
                                token = tsb.ToString();
                                tsb.Length = 0;

                                infix.Add(token);
                                infix.Add(sep);

                                expIndex = expSepIndex;
                                isSep = true;

                                break;
                            }
                        }
                    }

                    if (!isSep)
                    {
                        tsb.Append(e);
                    }
                }
            }

            if (tsb.Length > 0)
            {
                infix.Add(tsb.ToString());
            }

            infix.Add(")");

            _infix = infix.ToArray();
        }


        /// <summary>
        /// Convert Infix to Postfix.
        /// </summary>
        private void InfixToPostfix()
        {
            _postfix = new string[_infix.Length];

            int index = 0;
            while (index < _infix.Length)
            {
                _infix[index] = _infix[index].Trim();

                // if token is ( push on stack
                if (_infix[index] == "(")
                {
                    _stack.Push(_infix[index++]);
                    continue;
                }

                // if binary or unary operator, write to Postfix array
                if (!IsBinaryOperator(_infix[index]) && !IsUnaryOperator(_infix[index]))
                {
                    if (_infix[index] != "(" && _infix[index] != ")")
                    {
                        _postfix[_postIndex++] = _infix[index++];
                        continue;
                    }
                }
                else // operand
                {
                    // if stack empty, push token
                    if (_stack.Count == 0)
                    {
                        _stack.Push(_infix[index++]);
                        continue;
                    }
                    else
                    {
                        while (true)
                        {
                            //	if token has higher precedance than top of stack, push token
                            if ((int)_precedence[_infix[index]] < (int)_precedence[_stack.Peek()])
                            {
                                _stack.Push(_infix[index++]);
                                break;
                            }
                            else
                            {
                                // else pop operator add to postfix. continue with comparison
                                _postfix[_postIndex++] = _stack.Pop();
                            }
                        }
                    }
                }


                // If ")" then pop the stack and add popped token to postfix until "(" is reached.
                // A "(" is popped but not added to postfix.
                if (_infix[index] == ")")
                {
                    string ch;
                    while ((ch = _stack.Pop()) != "(")
                    {
                        _postfix[_postIndex++] = ch;
                    }
                    index++;
                }
            }
        }


        // Check if the token is a binary operator
        private bool IsBinaryOperator(string str)
        {
            foreach (string s in _binaryOperators)
            {
                if (s == str)
                {
                    return true;
                }
            }
            return false;
        }


        // Check if the token is a unary operator
        private bool IsUnaryOperator(string str)
        {
            foreach (string s in _unaryOperators)
            {
                if (s == str)
                {
                    return true;
                }
            }
            return false;
        }

        static public bool EvaluateToBool(object dataContext, string exp)
        {
            return Boolean.Parse(Evaluate(dataContext, exp));
        }

        static public string Evaluate(object dataContext, string exp)
        {

            PDOExpressionEvaluator ee = new PDOExpressionEvaluator();
            ee._dataContext = dataContext;
            ee.Tokenize(exp);
            return ee.Solve();
        }


        string GetIndexExpression(string token1)
        {
            string result = token1;
            while (_stack.Peek() != "[")
            {
                result = _stack.Pop() + result;
            }
            result = _stack.Pop() + result;
            result = _stack.Pop() + result;

            return result;
        }

        private string GetNextOperand()
        {
            string operand = null;

            if (_stack.Count > 0)
            {
                operand = _stack.Pop();

                if (operand.StartsWith("."))
                {
                    operand = _stack.Pop() + operand;
                }

                if (operand.StartsWith("]"))
                {
                    operand = GetIndexExpression(operand);
                }
            }
            return operand;
        }


        // Solve the Postfix expression
        public string Solve()
        {
            // clear the stack
            _stack.Clear();
            foreach (string str in _postfix)
            {
                // if token empty continue. This should not happen
                if (str == "")
                    continue;

                if (str == null)
                {
                    // Check if there's anything interesting left on the stack
                    string operand = GetNextOperand();
                    if (null != operand)
                    {
                        return GetOperand(operand).ToString();
                    }
                    break;
                }


                if (IsBinaryOperator(str))
                {
                    // Pop two operands from the stack
                    string operand1, operand2, result;

                    operand1 = GetNextOperand();

                    if (operand1 == null)
                    {
                        throw new InvalidOperationException(ExceptionMsgHeader + "malformed expression");
                    }


                    operand2 = GetNextOperand();

                    if (operand2 == null)
                    {
                        throw new InvalidOperationException(ExceptionMsgHeader + "malformed expression");
                    }

                    // Evaluate the two operands
                    result = Evaluate(str, operand1, operand2);

                    // push result on stack
                    _stack.Push(result);
                }
                else if (IsUnaryOperator(str))
                {
                    // pop one operand from the stack
                    string operand1, result;
                    if ((operand1 = _stack.Pop()) == null)
                    {
                        throw new InvalidOperationException(ExceptionMsgHeader + "malformed expression");
                    }

                    result = Evaluate(str, operand1);

                    // push result on stack
                    _stack.Push(result);
                }
                else
                {
                    // token is a operand - push onto stack
                    _stack.Push(str);
                    continue;
                }
            }

            string finalResult = _stack.Pop();

            if (_stack.Count != 0)
            {
                // stack should be empty when we're all done.
                throw new InvalidOperationException(ExceptionMsgHeader + "malformed expression");
            }

            return finalResult;
        }



        private object GetOperand(string operand)
        {
            object obj;

            if ((obj = GetTypedOperand(operand)) == null)
            {
                obj = ReflectionHelper.GetPropertyPathValue(_dataContext, operand);
                if (obj == null)
                {
                    obj = "null";
                }
            }

            return obj;

        }


        /// <summary>
        /// Evaluate the operation against the operand.
        /// </summary>
        /// <param name="opr">operation</param>
        /// <param name="operand1">operand1</param>
        /// <param name="operand2">operand2</param>
        /// <returns>result as string</returns>
        private string Evaluate(string opr, string operand1, string operand2)
        {
            object obj1 = GetOperand(operand1);
            object obj2 = GetOperand(operand2);

            // Evaluate
            return GetResult(opr, obj1, obj2);
        }


        /// <summary>
        /// Evaluate the operation against the operand.
        /// </summary>
        /// <param name="opr">operation</param>
        /// <param name="operand1">operand</param>
        /// <returns>result</returns>
        private string Evaluate(string opr, string operand)
        {
            object obj = GetOperand(operand);

            // Evaluate
            return GetResult(opr, obj);
        }


        private bool IsNumericType(TypeCode tc)
        {
            // Internally we only support Int64 or Double as numeric types, so that's all we need to check for.
            return (tc == TypeCode.Int64 || tc == TypeCode.Double);
        }

        private bool IsStringType(TypeCode tc)
        {
            return (tc == TypeCode.String);
        }


        private bool IsBoolType(TypeCode tc)
        {
            return (tc == TypeCode.Boolean);
        }



        /// <summary>
        /// Get the result of operation of operands.
        /// </summary>
        /// <param name="opr">operation</param>
        /// <param name="obj1">operand1</param>
        /// <param name="obj2">operand2</param>
        /// <returns>result</returns>
        private string GetResult(string opr, object obj1, object obj2)
        {
            Type t1 = obj1.GetType();
            Type t2 = obj2.GetType();

            TypeCode tc1 = Type.GetTypeCode(t1);
            TypeCode tc2 = Type.GetTypeCode(t2);

            string operand1 = obj1.ToString();
            string operand2 = obj2.ToString();

            switch (opr)
            {
                case "+":
                    if (IsStringType(tc1) && IsStringType(tc2))
                    {
                        string result = operand2 + operand1;

                        return operand2 + operand1;
                    }
                    else if (IsNumericType(tc1) && IsNumericType(tc2))
                    {
                        // Operate on both integers and floats as double
                        Double result;
                        result = Double.Parse(operand2) + Double.Parse(operand1);
                        return result.ToString();
                    }
                    else
                    {
                        throw new InvalidOperationException(ExceptionMsgHeader + "invalid operation \"" + opr + "\" on type 1 = \"" + tc1.ToString() + "\", type 2 =\"" + tc2.ToString() + "\"");
                    }

                case "-":
                    if (IsNumericType(tc1) && IsNumericType(tc2))
                    {
                        // Operate on both integers and floats as double
                        Double result;
                        result = Double.Parse(operand2) - Double.Parse(operand1);
                        return result.ToString();
                    }
                    else
                    {
                        throw new InvalidOperationException(ExceptionMsgHeader + "invalid operation \"" + opr + "\" on type 1 = \"" + tc1.ToString() + "\", type 2 =\"" + tc2.ToString() + "\"");
                    }

                case "*":
                    if (IsNumericType(tc1) && IsNumericType(tc2))
                    {
                        // Operate on both integers and floats as double
                        Double result;
                        result = Double.Parse(operand1) * Double.Parse(operand2);
                        return result.ToString();
                    }
                    else
                    {
                        throw new InvalidOperationException(ExceptionMsgHeader + "invalid operation \"" + opr + "\" on type 1 = \"" + tc1.ToString() + "\", type 2 =\"" + tc2.ToString() + "\"");
                    }

                case "/":
                    if (IsNumericType(tc1) && IsNumericType(tc2))
                    {
                        // Operate on both integers and floats as double
                        Double result;
                        result = Double.Parse(operand2) / Double.Parse(operand1);
                        return result.ToString();
                    }
                    else
                    {
                        throw new InvalidOperationException(ExceptionMsgHeader + "invalid operation \"" + opr + "\" on type 1 = \"" + tc1.ToString() + "\", type 2 =\"" + tc2.ToString() + "\"");
                    }

                case "&&":
                    if (IsBoolType(tc1) && IsBoolType(tc2))
                    {
                        return ((bool)obj1 && (bool)obj2).ToString();
                    }
                    else
                    {
                        throw new InvalidOperationException(ExceptionMsgHeader + "invalid operation \"" + opr + "\" on type 1 = \"" + tc1.ToString() + "\", type 2 =\"" + tc2.ToString() + "\"");
                    }

                case "||":
                    if (IsBoolType(tc1) && IsBoolType(tc2))
                    {
                        return ((bool)obj1 || (bool)obj2).ToString();
                    }
                    else
                    {
                        throw new InvalidOperationException(ExceptionMsgHeader + "invalid operation \"" + opr + "\" on type 1 = \"" + tc1.ToString() + "\", type 2 =\"" + tc2.ToString() + "\"");
                    }

                case "=":
                    operand2 = operand1;
                    return operand2;

                case "==": // just do string compare
                    return (operand1 == operand2).ToString();

                case "!=": // just do string compare
                    return (operand1 != operand2).ToString();

                default:
                    throw new InvalidOperationException(ExceptionMsgHeader + "unknown operator \"" + opr + "\"");
            }
        }


        // Get the result of operation of operands
        private string GetResult(string opr, object obj1)
        {
            TypeCode tc = Type.GetTypeCode(obj1.GetType());
            string operand1 = obj1.ToString();

            switch (opr)
            {
                case "!":
                    if (IsBoolType(tc))
                    {
                        return (!(bool)obj1).ToString();
                    }
                    else
                    {
                        throw new InvalidOperationException(ExceptionMsgHeader + "invalid operation \"" + opr + "\" on type = \"" + tc.ToString() + "\"");
                    }

                default:
                    throw new InvalidOperationException(ExceptionMsgHeader + "unknown operator \"" + opr + "\"");
            }
        }


        /// <summary>
        /// Get the type of the operand.
        /// If operand is of none of these types, it is an identifier.
        /// </summary>
        /// <param name="operand">operand as string</param>
        /// <returns>operand coerced to appropriate type</returns>
        private object GetTypedOperand(string operand)
        {
            string long_pattern = "(^[0-9]+$)+";
            string double_pattern = "^[0-9]*\\.?[0-9]+$";
            string string_pattern = "\"[^\"\\\\\\r\\n]*(?:\\\\.[^\"\\\\\\r\\n]*)*\"";
            string bool_pattern = "(true|false)+";

            object result = null;
            try
            {
                if (Regex.IsMatch(operand, long_pattern))
                    result = Int64.Parse(operand);

                if (Regex.IsMatch(operand, double_pattern)) // need to exclude alphas!
                    result = Double.Parse(operand);

                //if (Regex.IsMatch(operand, string_pattern)) // need a regex to replace following code
                // should be able to replace with... string regex = "\"((\\\")|[^\"(\\\")])+\"";
                if (operand.StartsWith("\"") && operand.EndsWith("\"") && !operand.Substring(1, operand.Length - 2).Contains("\""))
                    result = operand.Substring(1, operand.Length - 2);

                if (Regex.IsMatch(operand.ToLower(), bool_pattern))
                    result = Boolean.Parse(operand);
            }
            catch
            {
                result = null;
            }
            return result;
        }

    }

}
